/**
 * A basic java property-based configuration base class that automates the
 * loading of properties into a configuration object. This object allows a
 * quick and thin stub implementation of Configuration Services
 */
package za.org.jcicada.cfgmngr.configuration;

import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/** Base configuration class. Takes care of loading and parsing a configuration
 * properties file. To use, simply extend from this class and make fields that
 * will be loaded from the properties file publc. Then assign values to these
 * fields in the properties file, making sure the field name used in the
 * properties file is the same as the field name in the extended class. This
 * class uses reflection to populate the configuration from the properties file.
 */
abstract public class BaseConfiguration {
    /** class logger */
    private final Log logger = LogFactory.getLog(BaseConfiguration.class);
    /** The properties data store */
    Properties properties = new Properties();

    /**
     * Loads a properties file and ignores commented lines and empty lines.
     * Also parses the file and populate the configuration object fields.
     * Private and final fields are not populated. If a field cannor be
     * populated for some reason, it retains the value it had before this
     * method was called. Field types supported:
     * o boolean
     * o int
     * o double
     * o String
     * o List<String>
     * @return true if the file was successfully loaded and parsed.
     * @return false if any operation failed
     */
    public boolean loadConfiguration(String fileName){
        boolean result = false;
        try {
            InputStream propertiesFile = new FileInputStream(fileName);
            properties.load(propertiesFile);
            result = parseConfiguration();
        } catch (Exception ex){
            logger.debug("Could not load properties from " + fileName + ". Exception: " + ex);
            return false;
        }

        return result;
    }
    

    /**
     * Loads a properties file and ignores commented lines and empty lines.
     * Also parses the file and populate the configuration object fields.
     * Private and final fields are not populated. If a field cannor be
     * populated for some reason, it retains the value it had before this
     * method was called. Field types supported:
     * o boolean
     * o int
     * o double
     * o String
     * o List<String>
     * @return true if the file was successfully loaded and parsed.
     * @return false if any operation failed
     */
    public boolean loadProperties(Properties properties) {
        boolean result = false;
        if (properties == null) {
            throw new IllegalArgumentException("Can't load from null properties");
        }
        try {
            this.properties = properties;
            result = parseConfiguration();
        } catch (Exception ex){
            logger.debug("Could not load properties. Exception: " + ex);
            return false;
        }

        return result;
    }    

    /**
     * Attempts to parse the specified field from the loaded properties.
     * @return The String representation of the field
     * @return null if the field could not be found.
     */
    public String parseField(String field){
        return properties.getProperty(field);
    }

    /**
     * Parses the specified field and returns the String representation.
     * @return String representation on success
     * @return null if the field could not be found
     */
    public String parseString(String field){
        return parseField(field);
    }

    /**
     * Parses the specified field and returns the Double representation.
     * @return Double representation on success
     * @return null if the field could not be found
     */
    public Double parseDouble(String field){
        return new Double(parseField(field));
    }

    /**
     * Parses the specified field and returns the Integer representation.
     * @return Integer representation on success
     * @return null if the field could not be found
     */
    public Integer parseInteger(String field){
        return new Integer(parseField(field));
    }

    /**
     * Parses the specified field and returns the Long representation.
     * @return Long representation on success
     * @return null if the field could not be found
     */
    public Long parseLong(String field){
        return new Long(parseField(field));
    }

    /**
     * Parses the specified field and returns the Boolean representation.
     * @return Boolean representation on success
     * @return null if the field could not be found
     */
    public Boolean parseBoolean(String field){
        return new Boolean(parseField(field));
    }

    /**
     * Parses the specified field and returns a List representation.
     * @return List representation on success, containing String objects
     * @return null if the field could not be found
     */
    public List<String> parseList(String field){
        List<String> list = new ArrayList<String>();
        String[] params = parseString(field).split(",");
        for (String s : params){
            list.add(s);
        }
        return list;
    }

    /**
     * Uses reflection on the configuration object to determine expected
     * field names to be populated from the loaded configuration. Makes type
     * decisions based on the configuration field types and parses the
     * properties, populating the configuration fields using the correct
     * type transform. Skips final and private fields.
     * @return true if no errors occurred.
     * @return false if an error occurred. Fields are still populated where
     * possible
     */
    @SuppressWarnings("unchecked")
	public boolean parseConfiguration(){
        boolean result = true;
        Field[] fields = this.getClass().getFields();
        for (Field f : fields){
            try{
                Class type = f.getType();
                
                // Skip final fields
                if ((Modifier.isFinal(f.getModifiers())) ||
                    (Modifier.isPrivate(f.getModifiers()))){
                    continue;
                }

                if (type.getCanonicalName().equals(int.class.getCanonicalName())){
                    f.setInt(this, parseInteger(f.getName().trim()));
                } else
                if (type.getCanonicalName().equals(long.class.getCanonicalName())) {
                	f.setLong(this, parseLong(f.getName().trim()));
                } else
                if (type.getCanonicalName().equals(boolean.class.getCanonicalName())){
                    f.setBoolean(this, parseBoolean(f.getName().trim()));
                } else
                if (type.getCanonicalName().equals(double.class.getCanonicalName())){
                    f.setDouble(this, parseDouble(f.getName().trim()));
                } else
                if (type.getCanonicalName().equals(String.class.getCanonicalName())){
                    /* Since an empty assignment will be interpreted as an empty
                     * string and still assigned here, check before-hand. If the
                     * property assignment is empty, do not parse the string
                     field but leave the value to the default constructed by the
                     object */
                    if (!properties.getProperty(f.getName().trim()).equals("")){
                        f.set(this, parseString(f.getName().trim()));
                    }
                } else
                if (type.getCanonicalName().equals(List.class.getCanonicalName())){
                    /* Since an empty assignment will be interpreted as an empty
                     * string and still assigned here, check before-hand. If the
                     * property assignment is empty, do not parse the list field
                     * but leave the value to the default constructed by the
                     * object */
                    if (!properties.getProperty(f.getName().trim()).equals("")){
                        f.set(this, parseList(f.getName().trim()));
                    }
                }
            } catch (Exception ex){
                result = false;
                // Skip this field. Log a warning.
                logger.warn("Could not populate field " + f.getName() + " - the value might not be valid. Field left at constructor default value. Exception was: " + ex);
            }
        }
        return result;
    }
}
